package de.akabahn.ebd.comm.telnet;

import static de.akabahn.ebd.comm.Util.assertInRange;
import static de.akabahn.ebd.comm.Util.assertNotNull;
import static de.akabahn.ebd.comm.Util.waitForLatch;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

import org.apache.commons.net.telnet.TelnetClient;

import de.akabahn.ebd.comm.ConnectionException;
import de.akabahn.ebd.comm.ConnectionObserver;
import de.akabahn.ebd.comm.ObservableConnection;

/**
 * Client implementation for a telnet connection to a remote server. Extends
 * from <code>ObservableConnection</code> so any class that is interested in the
 * current connection status and in the change of this status, may register
 * itself as an observer so it gets notified on a connection state change.
 * 
 * @author Peter Eimann
 * @since 0.2
 * @version 0.2
 */
public class TelnetConnectionClient extends ObservableConnection {

	private String server;
	private String eobChars;
	private String expectedWelcomeMessage;
	private int port;

	private TelnetClient telnetClient;
	private InputStream inputStream;
	private OutputStream outputStream;
	private ConnectionObserver connectionObserver;
	private TelnetBridge telnetBridge;

	private ExecutorService executor;

	/**
	 * Create and configure the client. To connect to the telnet server, you
	 * have to call <code>connect()</code> first.
	 * 
	 * @param server
	 *            The server to connect to
	 * @param port
	 *            The port to connect to
	 * @param eobChars
	 *            A string representing the characters which identify the end of
	 *            the response to a request
	 * @param expectedWelcomeMessage
	 *            The expected message that the telnet server returns after
	 *            connecting (may be null if no message is returned)
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @see #connect()
	 */
	public TelnetConnectionClient(String server, int port, String eobChars, String expectedWelcomeMessage) {

		assertNotNull(server, "server");
		assertInRange(port, "port", 0, 65535);
		assertNotNull(eobChars, "eobChars");

		this.server = server;
		this.port = port;
		this.eobChars = eobChars;
		this.expectedWelcomeMessage = expectedWelcomeMessage;

		telnetClient = new TelnetClient();
	}

	/**
	 * Connect to the telnet server using the telnet bridge. If the connection
	 * could be established, the <code>ConnectionObserver</code> is started to
	 * observe changes in the connection status.
	 * 
	 * @throws ConnectionException
	 * @see TelnetBridge
	 * @see ConnectionObserver
	 */
	public synchronized void connect() throws ConnectionException {
		CountDownLatch latch = new CountDownLatch(1);
		executor = Executors.newCachedThreadPool();

		if (telnetBridge == null) {
			telnetBridge = new TelnetBridge(telnetClient, server, port, latch);
			executor.execute(telnetBridge);

			if (!waitForLatch(latch, 30000)) {
				throw new ConnectionException("Connect timed out");
			}

			if (telnetBridge.getThreadException() != null) {
				throw new ConnectionException("Connect failed", telnetBridge.getThreadException());
			}

			try {
				inputStream = telnetClient.getInputStream();
				outputStream = telnetClient.getOutputStream();
			} catch (Exception e) {
				throw new ConnectionException("Get input or output stream failed", e);
			}

			if (expectedWelcomeMessage != null) {
				checkWelcomeMessage();
			}
		}

		if (connectionObserver == null) {
			connectionObserver = new ConnectionObserver(this);
			executor.execute(connectionObserver);
		}
	}

	/**
	 * Check if the response which has been received from the telnet server
	 * starts with the expected welcome message. This message is normally sent
	 * after a connection has been established.
	 * 
	 * @throws ConnectionException
	 *             Thrown if the welcome message hasn't been received
	 */
	void checkWelcomeMessage() throws ConnectionException {
		String welcomeMessage = read();

		if (!welcomeMessage.startsWith(expectedWelcomeMessage)) {
			throw new ConnectionException("Unexpected welcome message: " + welcomeMessage);
		}
	}

	/**
	 * Close the current connection by stopping the <code>TelnetBridge</code>
	 * and the <code>ConnectionObserver</code> and by shutting down the cached
	 * thread pool.
	 * 
	 * @throws ConnectionException
	 */
	public synchronized void disconnect() throws ConnectionException {
		if (telnetBridge != null) {
			telnetBridge.prepareToStop();
		}
		if (connectionObserver != null) {
			connectionObserver.prepareToStop();
		}

		executor.shutdownNow();
		try {
			if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
				throw new ConnectionException(
						"At lease one thread is still alive",
						telnetBridge.getThreadException());
			}
		} catch (InterruptedException e) {
		} finally {
			telnetBridge = null;
			connectionObserver = null;
			executor = null;
		}
	}

	/**
	 * Checks if a connection to the telnet server exists or not by calling
	 * <code>isAvailable()</code> on the <code>TelnetClient</code>.
	 * 
	 * @return <code>true</code> if a connection exists, <code>false</code>
	 *         otherwise
	 */
	@Override
	public synchronized boolean isConnected() {
		return telnetClient.isAvailable();
	}

	/**
	 * Reestablish the telnet connection by calling <code>disconnect()</code>
	 * and <code>connect()</code>.
	 */
	@Override
	public synchronized void reconnect() {
		try {
			disconnect();
		} catch (RuntimeException e) {
			// Ignore exceptions (maybe was already disconnected)
		}
		connect();
	}

	/**
	 * Write the content of <code>buffer</code> to the telnet server. Adds a new
	 * line (<code>\n</code>) at the end and flushes the output stream.
	 * 
	 * @param buffer
	 *            The content to sent
	 * @throws ConnectionException
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 */
	public synchronized void write(String buffer) throws ConnectionException {
		assertNotNull(buffer, "buffer");

		try {
			outputStream.write(buffer.getBytes());
			outputStream.write('\n');
			outputStream.flush();
		} catch (Exception e) {
			throw new ConnectionException("Write failed", e);
		}
	}

	/**
	 * Read a response from the telnet server. Reading the data is done in a
	 * separate thread to be able to detect a timeout if no or incomplete data
	 * is received. The read operation on the input stream is repeated until the
	 * current response ends with the <code>endOfBuffer</code> characters or
	 * until the timeout of 5 seconds is reached.
	 * 
	 * @return The received response (without the <code>endOfBuffer</code>
	 *         characters)
	 * @see #createReader()
	 * @throws ConnectionException
	 */
	public synchronized String read() throws ConnectionException {
		String response = null;

		FutureTask<String> reader = new FutureTask<String>(createReader());
		executor.execute(reader);

		try {
			response = reader.get(5, TimeUnit.SECONDS);
		} catch (Exception e) {
			throw new ConnectionException("Read failed", e);
		}

		return response.substring(0, response.length() - eobChars.length());
	}

	/**
	 * Create the reader that reads the response from the telnet server.
	 * 
	 * @return The reader
	 */
	Callable<String> createReader() {
		return new Callable<String>() {
			public String call() throws Exception {
				int oneChar = -1;
				StringBuilder builder = new StringBuilder();

				while ((oneChar = inputStream.read()) != -1) {
					builder.append((char) oneChar);
					if (builder.indexOf(eobChars) != -1) {
						break;
					}
				}

				return builder.toString();
			}
		};
	}

	/**
	 * Return a string representation of this instance
	 */
	public String toString() {
		return "TelnetConnection [IP=" + server + ", PO=" + port + ", CO="
				+ isConnected() + ", WM=" + expectedWelcomeMessage + ", EOB="
				+ Arrays.toString(eobChars.getBytes()) + "]";
	}
}
